#ifndef __SHADER_H__
#define __SHADER_H__

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <unordered_map>

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

class Shader
{
private:
    std::unordered_map<std::string, unsigned int> paramMap;

    std::string readFileText(const char* name);
    unsigned int genShader(const int type, const std::string &src);
    unsigned int genProgram(unsigned int vs, unsigned int fs);
    unsigned int fetchParamID(const std::string &name);

    unsigned int programID;  
public:
    Shader(const char* vertexPath, const char* fragmentPath);
    bool ready();
    void use();

    void setBool(const std::string &name, bool val);
    void setInt(const std::string &name, int val);
    void setFloat(const std::string &name, float val);
    void setMat4(const std::string &name, glm::mat4 &mat);
    void setMat3(const std::string &name, glm::mat3 &mat);
    void setVec3(const std::string &name, glm::vec3 &val);
};

std::string Shader::readFileText(const char* name)
{
    std::string res;
    std::fstream file;

    file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    file.open(name);
    std::stringstream content;
    content << file.rdbuf();
    file.close();
    res = content.str();

    return res;
}

unsigned int Shader::genShader(int type, const std::string &src)
{
    const char* csrc = src.c_str();
    unsigned int res = 0;
    res = glCreateShader(type);
    glShaderSource(res, 1, &csrc, NULL);
    glCompileShader(res);

    int success;
    glGetShaderiv(res, GL_COMPILE_STATUS, &success);
    if(!success)
    {
        const char* typeStr = type == GL_VERTEX_SHADER ? "Vertex Shader" : "Fragment Shader";
        char infoLog[512];
        glGetShaderInfoLog(res, 512, NULL, infoLog);
        std::cout << "ERROR: Failed to compile shader: " << typeStr << std::endl;
        std::cout << infoLog << std::endl;
        glDeleteShader(res);
    }
    return res;
}

unsigned int Shader::genProgram(unsigned int vs, unsigned int fs)
{
    unsigned int program = glCreateProgram();
    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    int success = 0;
    char infoLog[512];
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if(!success)
    {
        glGetProgramInfoLog(program, 512, NULL, infoLog);
        std::cout << "ERROR: Failed to create shader program. " << std::endl;
        std::cout << infoLog << std::endl;
        glDeleteProgram(program);
        program = 0;
    }

    glDeleteShader(vs);
    glDeleteShader(fs);
    return program;
}

Shader::Shader(const char* vertexPath, const char* fragmentPath){
    std::string vShaderSrc;
    std::string fShaderSrc;

    std::fstream vShaderFile, fShaderFile;
    try {
        vShaderSrc = this->readFileText(vertexPath);
        fShaderSrc = this->readFileText(fragmentPath);

        unsigned int vShader = this->genShader(GL_VERTEX_SHADER, vShaderSrc);
        unsigned int fShader = this->genShader(GL_FRAGMENT_SHADER, fShaderSrc);
        this->programID = this->genProgram(vShader, fShader);

        glDeleteShader(vShader);
        glDeleteShader(fShader);
    } catch (std::ifstream::failure& e)
    {
        std::cerr << "ERROR: FAILED_TO_READ_SHADER_FILE: " << e.what() << std::endl;
    }
}

bool Shader::ready()
{
    return this->programID != 0;
}

void Shader::use()
{
    glUseProgram(this->programID);
}

unsigned int Shader::fetchParamID(const std::string &name)
{
    if(paramMap.find(name) == paramMap.end())
    {
        unsigned int paramID = glGetUniformLocation(programID, name.c_str());
        paramMap.insert(std::pair<std::string, unsigned int>(name, paramID));
    }
    return paramMap[name];
}

void Shader::setBool(const std::string &name, bool val)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniform1i(paramID, val);
}

void Shader::setInt(const std::string &name, int val)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniform1i(paramID, val);
}

void Shader::setFloat(const std::string &name, float val)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniform1f(paramID, val);
}

void Shader::setMat4(const std::string &name, glm::mat4 &mat)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniformMatrix4fv(paramID, 1, GL_FALSE, glm::value_ptr(mat));
}

void Shader::setMat3(const std::string &name, glm::mat3 &mat)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniformMatrix3fv(paramID, 1, GL_FALSE, glm::value_ptr(mat));
}

void Shader::setVec3(const std::string &name, glm::vec3 &f3)
{
    unsigned int paramID = this->fetchParamID(name);
    glUniform3fv(paramID, 1, glm::value_ptr(f3));
}
#endif